/*
 * TODO
 * moving objects from one layer to another
 * adding new layers
 * 'groups' of objects (allow functions to be applied to lists; use jQuery?)
 */
function LCanvas(num_layers, width, height, container) {
	this.container = container;
	this.layers = [];
	this.ctxs = [];
	this.objects = [];
	this.animations = [];
	this.animation_count = 0;
	this.timer = null;

	for (var i = 0; i < num_layers; i++) {
		this.add_layer(width, height);
	}
}

LCanvas.prototype.FRAME_DELAY = 38;

//return a unique ID (string)
LCanvas.prototype.unique_id = (function() {
	var id = 0;
	return function() {
		return (id++).toString();
	};
})();
LCanvas.prototype.add_layer = function(width, height) {
	return this.create_layer(this.layers.length, width, height);
};
LCanvas.prototype.create_layer = function(layer_number, width, height) {
	//if there is already a layer for this layer_number, do nothing (could be changed to reinitialize it...)
	if (this.layers[layer_number] !== undefined && this.layers[layer_number].length > 0)
		return;

	//if no dimensions passed in, use default 800x600
	if (width === undefined)
		width = 800;
	if (height === undefined)
		height = 600;

	//initialize empty objects container
	this.objects[layer_number] = {};
	//initialize empty animations container
	this.animations[layer_number] = {};

	//create canvas object
	var layer = $('<canvas>');
	layer.addClass('canvas_layer');
	//need to set width and height on canvas directly, not just in css
	layer[0].width = width
	layer[0].height = height
	layer.css('z-index', layer_number);
	//keep a reference to $(canvas)
	this.layers[layer_number] = layer;

	var ctx = layer[0].getContext('2d');
	//extend ctx with some useful functions
	for (var i in ctx_functions) {
		ctx[i] = ctx_functions[i];
	}
	//give 2D ctx some convenient references
	ctx.layer_number = layer_number;
	ctx.LCanvas = this;
	ctx.width = width;
	ctx.height = height;
	//keep a reference to the 2D context
	this.ctxs[layer_number] = ctx;

	$(this.container).append(layer);

	return layer;
};
//adds a callback to be evaluated every this.FRAME_DELAY milliseconds
LCanvas.prototype.add_animation = function(frame_func, layer_number) {
	var anim_id = this.unique_id();
	this.animations[layer_number][anim_id] = frame_func;
	if (this.animation_count == 0) {
		var lcanv = this;
		this.timer = window.setInterval(function() {lcanv.tick.call(lcanv);}, this.FRAME_DELAY);
	}
	//keep track of the number of animations so we can stop the frame delay timer when there aren't any callbacks to call
	this.animation_count++;
	return anim_id;
};
LCanvas.prototype.remove_animation = function(layer_number, anim_id) {
	if (!this.animations[layer_number][anim_id])
		return;
	//delete the callback function from the animations list
	delete this.animations[layer_number][anim_id];
	//keep track of the number of animations so we can stop the frame delay timer when there aren't any callbacks to call
	this.animation_count--;
};
//called every this.FRAME_DELAY milliseconds; checks for animations that need to be updated
LCanvas.prototype.tick = function() {
	//stop the timer if there aren't any callbacks in the list
	if (this.animation_count == 0) {
		window.clearInterval(this.timer);
		return;
	}

	var redraw_layer;
	for (var layer_num in this.animations) {
		redraw_layer = false;
		for (var id in this.animations[layer_num]) {
			this.animations[layer_num][id].call(this);
			redraw_layer = true;
		}
		//only redraw the layer if there's an animation for that layer
		if (redraw_layer)
			this.ctxs[layer_num].redraw();
	}
};
//some useful functions to have available on the context object itself
var ctx_functions = {
	//remove all 'objects' associated with this context, blank the canvas
	clear: function() {
		this.reset();
		this.LCanvas.objects[this.layer_number] = {};
	},
	//blank the canvas associated with this context
	reset: function() {
		this.clearRect(0, 0, this.width, this.height);
	},
	//blank this canvas and redraw everything on it
	redraw: function() {
		this.reset();
		for (var i in this.LCanvas.objects[this.layer_number]) {
			this.apply_methods(this.LCanvas.objects[this.layer_number][i]);
		}
	},
	//execute one of this context's native drawing functions and log the function name and arguments in the 'objects' list for this layer
	draw: function(obj) {
		if (obj === undefined || !obj.methods)
			throw 'missing params';

		var id = this.LCanvas.unique_id();
		this.LCanvas.objects[this.layer_number][id] = obj;
		this.apply_methods(obj);
		return id;
	},
	//create a callback function and add it to the animations list
	//delta should be a list which can be mapped surjectively onto the arguments used for creating the object associated with obj_id.
	//delta can contain integers or functions.
	//integers will be added to previous args, functions applied to previous args
	animate: function(obj_id, delta) {
		var obj = this.LCanvas.objects[this.layer_number][obj_id];
		if (obj === undefined || !delta.length)
			return;

		var layer = this;
		return this.LCanvas.add_animation(function() {layer.frame(obj, delta);}, this.layer_number);
	},
	//gradually transform an object to the given state (for now, only constant delta)
	//provide an ending state and a duration and the delta functions will be created for you
	//TODO fire event when finished (prolly need anim ID before function creation, e.g. create animation object?)
	//TODO use dynamic time delta, i.e. not a fixed amount of time between frames
	animate_to: function(obj_id, end_state, duration) {
		var obj = this.LCanvas.objects[this.layer_number][obj_id];
		if (obj === undefined || !end_state.length)
			return;

		if (duration <= 0)
			duration = 1;

		var step_size;
		var limit_func;
		var num_steps = Math.max(duration / this.LCanvas.FRAME_DELAY, 1);
		var delta = [];
		for (var i in end_state) {
			if (end_state[i] === null) {
				delta[i] = null;
				continue;
			}
			delta[i] = [];
			var j;
			for (j in end_state[i]) {
				step_size = (end_state[i][j] - obj.args[i][j]) / num_steps;
				if (step_size >= 0)
					limit_func = 'min';
				else
					limit_func = 'max';
				delta[i][j] = (function() {
					var step = step_size;
					var lim = limit_func;
					var end = end_state[i][j];
					return function(prev) {
						return Math[lim].call(null, prev + step, end);
					};
				})();
			}
		}
		var anim = this.animate(obj_id, delta);
		var lcanv = this.LCanvas;
		var layer_num = this.layer_number;
		//TODO use events for this
		window.setTimeout(function() { lcanv.remove_animation(layer_num, anim); }, duration + 500);
	},
	//remove an animation callback from the list
	stop_animation: function(anim_id) {
		this.LCanvas.remove_animation(this.layer_number, anim_id);
	},
	//default callback for an animation (called by LCanvas.tick)
	frame: function(obj, delta) {
		for (var i in delta) {
			if (delta[i] === undefined || delta[i] === null || !delta[i].length)
				continue;

			for (var j in delta[i]) {
				if (typeof delta[i][j] == 'number')
					obj.args[i][j] += delta[i][j];
				else if (typeof delta[i][j] == 'function')
					obj.args[i][j] = delta[i][j].apply(this, [obj.args[i][j]]);
				else
					obj.args[i][j] = delta[i][j];
			}
		}
	},
	//apply the function passed in the same format as is stored in LCanvas.objects
	apply_methods: function(obj) {
		if (obj.fillStyle)
			this.fillStyle = obj.fillStyle;
		var empty_args = [];
		for (var i in obj.methods) {
			if (!obj.args[i])
				this[obj.methods[i]].apply(this, empty_args);
			else
				this[obj.methods[i]].apply(this, obj.args[i]);
		}
	},
	//remove an object from this context/layer
	remove: function(id) {
		delete this.LCanvas.objects[this.layer_number][id];
		//redraw everything that's left in this layer
		this.redraw();
	}
};
